/*
 * @(#)DefaultServiceExecutor.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

import javax.script.ScriptException;

import ognl.OgnlException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.nhncorp.usf.core.config.runtime.Action;
import com.nhncorp.usf.core.config.runtime.BusinessObject;
import com.nhncorp.usf.core.config.runtime.BusinessObjectGroup;
import com.nhncorp.usf.core.config.runtime.Method;
import com.nhncorp.usf.core.config.runtime.ScriptInfo;
import com.nhncorp.usf.core.config.runtime.Service;
import com.nhncorp.usf.core.config.runtime.ServiceCondition;
import com.nhncorp.usf.core.exception.ExceptionHandler;
import com.nhncorp.usf.core.exception.InvocationException;
import com.nhncorp.usf.core.invoker.InvocationFuture;
import com.nhncorp.usf.core.invoker.InvocationFutureListener;
import com.nhncorp.usf.core.invoker.Invoker;
import com.nhncorp.usf.core.service.supplement.CacheEntryProvider;
import com.nhncorp.usf.core.service.supplement.CacheResultAcceptor;
import com.nhncorp.usf.core.service.supplement.MethodContextCleanup;
import com.nhncorp.usf.core.service.supplement.ParameterGenerator;
import com.nhncorp.usf.core.service.supplement.PostScriptExecutor;
import com.nhncorp.usf.core.service.supplement.PreScriptExecutor;
import com.nhncorp.usf.core.servlet.ServletContextHolder;
import com.nhncorp.usf.core.util.OgnlUtil;
import com.nhncorp.usf.core.util.ScriptUtil;

/**
 * default implementation of {@link ServiceExecutor}.
 *
 * @author Web Platform Development Team.
 */
@SuppressWarnings("unchecked")
public class DefaultServiceExecutor implements ServiceExecutor {
    private Log log = LogFactory.getLog(DefaultServiceExecutor.class);

    private ExceptionHandler exceptionHandler;

    /**
     * @param invoker Invoker
     * @see com.nhncorp.usf.core.service.ServiceExecutor#setInvoker(com.nhncorp.lucy.net.invoker.Invoker)
     */
    public void setInvoker(Invoker invoker) {
        // implement ServiceExecutor
    }

    /**
     * set ExceptionHandler to handler exceptions
     *
     * @param exceptionHandler ExceptionHandler
     */
    public void setExceptionHandler(ExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
    }

    /**
     * execute all business defined in {@link Action}.
     *
     * @param action        {@link Action}
     * @param dataStructure used parameters for executing methods of action
     * @return RESULT_STATUS
     * @throws Exception the Exception
     */
    @SuppressWarnings("deprecation")
	public RESULT_STATUS execute(Action action, Map<String, Object> dataStructure) throws Exception {
        ServiceExecutionContext context = new ServiceExecutionContext(dataStructure);
        context.setAction(action);
        context.setExceptionHandler(exceptionHandler);

        try {
            Collection<Service> services = new ArrayList<Service>();
            Collection<Service> boInfos = action.getBusinessObjectInfos();

            // if businessObject is not defined, it will be "success" status.
            Collection<Service> boGroupInfos = action.getBoGroupInfos();
            Collection<Service> boConditionInfos = action.getControlBlockInfos();

            if (boInfos.isEmpty() && boGroupInfos.isEmpty() && boConditionInfos.isEmpty()) {
                return RESULT_STATUS.success;
            }

            services.addAll(boInfos);

            if (boGroupInfos.size() > 0) {
                services.addAll(addBOList(boGroupInfos, dataStructure));
            }

            executeService(services, context);

            // bo or if loop
            for (Service controlService : boConditionInfos) {
                executeControlBlock(controlService, dataStructure, context);
            }
        } catch (OgnlException ognlException) {
            log.error(ognlException);
            return RESULT_STATUS.failure;
        } catch (InvocationException invocationException) {
            log.error(invocationException);
            return RESULT_STATUS.failure;
        } catch (InterruptedException interruptedException) {
            log.error(interruptedException);
            return RESULT_STATUS.failure;
        }
        return RESULT_STATUS.success;
    }

    /**
     * Adds the bo list.
     *
     * @param boGroupInfos  the bo group infos
     * @param dataStructure the data structure
     * @return the list<? extends service>
     * @throws ScriptException       the script exception
     * @throws NoSuchMethodException the no such method exception
     */
    @SuppressWarnings("deprecation")
	private List<? extends Service> addBOList(Collection<Service> boGroupInfos, Map<String, Object> dataStructure) throws ScriptException, NoSuchMethodException {
        List<Service> bos = new ArrayList<Service>();

        for (Service boGroupInfo : boGroupInfos) {
            BusinessObjectGroup boGroup = (BusinessObjectGroup)boGroupInfo;
            ArrayList boList = (ArrayList)ScriptUtil.execute(ServletContextHolder.getContextRealPath(),
                                                             boGroup.getScriptInfo(), dataStructure);
            Collection<BusinessObject> boInfos = boGroup.getBoInfos();

            for (Service bo : boInfos) {
                BusinessObject aBoInfo = (BusinessObject)bo;

                if (boList.contains(aBoInfo.getId())) {
                    bos.add(aBoInfo);
                }
            }
        }

        return bos;
    }

    /**
     * Execute service.
     *
     * @param boList        the bo list
     * @param context       the context
     * @throws Exception            the exception
     */
	private void executeService(Collection<Service> boList,
                                ServiceExecutionContext context) throws Exception {
        Map<MethodExecutor, MethodReturn> futuresMap = new HashMap<MethodExecutor, MethodReturn>();

        // method call
        Iterator bos = boList.iterator();

        while (bos.hasNext()) {
            BusinessObject bo = (BusinessObject)bos.next();
            context.setBo(bo);
            List<Service> methodList = new ArrayList<Service>();

            methodList.addAll(bo.getServiceInfos());

            Iterator methods = methodList.iterator();

            while (methods.hasNext()) {
                Method method = (Method)methods.next();
                context.setMethod(method);
                context.setMethodType(ServiceExecutionContext.MethodType.parallel);

                MethodExecutor methodExecutor = new MethodExecutor((ServiceExecutionContext)context.clone(), 3, 3);
                setSupplements(methodExecutor, method);

                if (methodExecutor.before()) {
                    futuresMap.put(methodExecutor, methodExecutor.execute(method));
                }
            }
        }

        final CountDownLatch numberOfReturnedValues = new CountDownLatch(futuresMap.size());

        for (MethodReturn methodReturn : futuresMap.values()) {
            methodReturn.addFutureListener(new InvocationFutureListener() {

                public void invocationComplete(InvocationFuture future) throws Exception {
                    numberOfReturnedValues.countDown();
                }

            });
        }

        numberOfReturnedValues.await();

        for (Map.Entry<MethodExecutor, MethodReturn> entry : futuresMap.entrySet()) {
            entry.getValue().finish();
            entry.getKey().after();
        }

        futuresMap.clear();
        futuresMap = null;
    }

    /**
     * Execute control block.
     *
     * @param controlService the control service
     * @param dataStructure  the data structure
     * @param context        the context
     * @throws Exception the exception
     */
    private void executeControlBlock(Service controlService, Map<String, Object> dataStructure,
                                     ServiceExecutionContext context) throws Exception {
        ScriptInfo commonScriptInfo = ((ServiceCondition)controlService).getCommonScriptInfo();

        if (commonScriptInfo != null) {
            ScriptUtil.processControlScript(ServletContextHolder.getContextRealPath(), commonScriptInfo, dataStructure);
        }

        processService(controlService, context);
    }

    /**
     * {@link BusinessObject} or {@link Method} or {@link ServiceCondition}
     *
     * @param service Service
     * @param context ServiceExecutionContext
     * @throws Exception the Exception
     */
    private void processService(Service service, ServiceExecutionContext context) throws Exception {
        switch (ServiceType.getServiceType(service)) {
            case bo:
                context.setBo((BusinessObject)service);
                processControlBO(service, context);
                break;
            case method:
                context.setMethodType(ServiceExecutionContext.MethodType.sequential);
                executeSequentialMethod((Method)service, context);
                break;
            case conditionBlock:
                List<Service> serviceList;
                ServiceCondition conditionBlockInfo = (ServiceCondition)service;

                if (conditionBlockInfo.isCheck()) {
                    ScriptInfo scriptInfo = conditionBlockInfo.getScriptInfo();

                    if (scriptInfo != null) {
                        ScriptUtil.processControlScript(ServletContextHolder.getContextRealPath(), scriptInfo,
                                                        context.getAttributes());
                    }

                    serviceList = getCheckedInfo((ServiceCondition)service, context.getAttributes());

                } else {
                    serviceList = conditionBlockInfo.getServiceInfos();
                }

                for (Service ser : serviceList) {
                    processService(ser, context);
                }

                break;
            default:
                throw new IllegalArgumentException("Unsupported ServiceType " + service);
        }
    }

    /**
     * Control Block 내부의 BO 처리
     *
     * @param serviceInfo Service
     * @param context     ServiceExecutionContext
     * @throws Exception the Exception
     */
    private void processControlBO(Service serviceInfo, ServiceExecutionContext context) throws Exception {
        for (Service ser : ((BusinessObject)serviceInfo).getServiceInfos()) {
            processService(ser, context);
        }
    }

    /**
     * Control 블럭 내부의 메소드 실행
     *
     * @param method  Method
     * @param context ServiceExecutionContext
     * @throws Exception the Exception
     */
    private void executeSequentialMethod(Method method, ServiceExecutionContext context) throws Exception {
        context.setMethod(method);
        MethodExecutor methodExecutor = new MethodExecutor(context, 3, 3);
        setSupplements(methodExecutor, method);

        if (methodExecutor.before()) {
            methodExecutor.execute(method).finish();
        }

        methodExecutor.after();
    }

    /**
     * 조건식 검사. true경우 등록된 {@link Service} 목록을 리턴
     *
     * @param conditionBlock   ServiceCondition
     * @param controlResultMap Map<String, Object>
     * @return List<Service>
     * @throws Exception the Exception
     */
    private List<Service> getCheckedInfo(ServiceCondition conditionBlock, Map<String, Object> controlResultMap) throws Exception {
        // if block
        if (conditionBlock.isCheck()) {
            Boolean isSatisfy = OgnlUtil.evaluateAndGet(conditionBlock.getCondition(), controlResultMap);

            if (!isSatisfy) {
                return Collections.emptyList();
            }
        }

        return conditionBlock.getServiceInfos();
    }

    /**
     * Sets the supplements.
     *
     * @param executor the executor
     * @param method   the method
     */
    private void setSupplements(MethodExecutor executor, Method method) {
        // before
        executor.addPreSupplement(new ParameterGenerator());

        // if (method.getPreScriptList().size() > 0)
        executor.addPreSupplement(new PreScriptExecutor());

        if (method.isUseCache()) {
            executor.addPreSupplement(new CacheEntryProvider());
            executor.addPostSupplement(new CacheResultAcceptor());
        }

        // after'
        // if (method.getPostScriptList().size() > 0)
        executor.addPostSupplement(new PostScriptExecutor());
        executor.addPostSupplement(new MethodContextCleanup());
    }
}
